// Copyright (c) 2011, Yves Goergen, http://unclassified.de
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are permitted
// provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice, this list of conditions
//   and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice, this list of
//   conditions and the following disclaimer in the documentation and/or other materials provided
//   with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Unclassified.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Windows.Forms.VisualStyles;

namespace Unclassified.UI
{
	public class ColorWheel : Control
	{
		private Bitmap wheelBitmap;
		private Bitmap slBitmap;
		private byte hue;
		private byte saturation;
		private byte lightness;
		private byte[] secondaryHues;
		private bool draggingHue;
		private bool draggingSL;

		public event EventHandler HueChanged;
		public event EventHandler SLChanged;

		public byte Hue
		{
			get
			{
				return hue;
			}
			set
			{
				if (value != hue)
				{
					hue = value;
					PrepareSLBitmap();
					Invalidate();
				}
			}
		}

		public byte Saturation
		{
			get
			{
				return saturation;
			}
			set
			{
				if (value != saturation)
				{
					saturation = value;
					Invalidate();
				}
			}
		}

		public byte Lightness
		{
			get
			{
				return lightness;
			}
			set
			{
				if (value != lightness)
				{
					lightness = value;
					Invalidate();
				}
			}
		}

		public byte[] SecondaryHues
		{
			get
			{
				return secondaryHues;
			}
			set
			{
				if ((value == null) != (secondaryHues == null) ||
					value == null ||
					value != null && value.Length != secondaryHues.Length)   // implies: secondaryHues != null
				{
					secondaryHues = value;
					Invalidate();
				}
				else if (value != null)
				{
					for (int i = 0; i < value.Length; i++)
					{
						if (value[i] != secondaryHues[i])
						{
							secondaryHues = value;
							Invalidate();
							break;
						}
					}
				}
			}
		}

		[Browsable(false)]
		[EditorBrowsable(EditorBrowsableState.Never)]
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public new bool TabStop
		{
			get { return base.TabStop; }
			set { base.TabStop = false; }
		}

		public ColorWheel()
		{
			SetStyle(ControlStyles.SupportsTransparentBackColor, true);

			DoubleBuffered = true;
			TabStop = false;

			PrepareWheelBitmap();
		}

		protected override void OnSizeChanged(EventArgs e)
		{
			PrepareWheelBitmap();
			PrepareSLBitmap();
			base.OnSizeChanged(e);
		}

		//protected override void OnPaintBackground(PaintEventArgs pevent)
		//{
		//    if (VisualStyleRenderer.IsSupported)
		//    {
		//        // The VisualStyleElement does not matter, we're only drawing the parent's background
		//        VisualStyleRenderer r = new VisualStyleRenderer(VisualStyleElement.Window.Dialog.Normal);
		//        r.DrawParentBackground(pevent.Graphics, ClientRectangle, this);
		//    }
		//    else
		//    {
		//        base.OnPaintBackground(pevent);
		//    }
		//}
		// NOTE: Other solution: http://dotnetrix.co.uk/custom.htm#tip3

		protected override void OnPaint(PaintEventArgs pe)
		{
			// Draw outer color wheel bitmap
			if (wheelBitmap != null)
				pe.Graphics.DrawImage(wheelBitmap, new Point());

			// Draw inner color bitmap
			if (slBitmap != null)
				pe.Graphics.DrawImage(slBitmap, new Point(slBitmap.Width / 2, slBitmap.Width / 2));

			pe.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

			// Draw hue marker
			double radAngle = (double) hue / 128 * Math.PI + Math.PI / 2;
			// sin(angle) = y / d
			// cos(angle) = x / d
			double d = 0.89 * wheelBitmap.Width / 2;
			int x = (int) Math.Round(d * Math.Cos(radAngle));
			int y = (int) -Math.Round(d * Math.Sin(radAngle));
			// Map center-relative coordinates to window coordinates
			x += wheelBitmap.Width / 2;
			y += wheelBitmap.Width / 2;
			Color c = ColorMath.ToGray(ColorMath.HslToRgb(new HslColor(hue, 255, 128))) > 128 ? Color.Black : Color.White;
			using (Pen p = new Pen(c))
			{
				pe.Graphics.DrawEllipse(p, x - 3, y - 3, 6, 6);
			}

			// Draw secondary hue markers
			if (secondaryHues != null)
			{
				foreach (byte sHue in secondaryHues)
				{
					radAngle = (double) sHue / 128 * Math.PI + Math.PI / 2;
					// sin(angle) = y / d
					// cos(angle) = x / d
					d = 0.89 * wheelBitmap.Width / 2;
					x = (int) Math.Round(d * Math.Cos(radAngle));
					y = (int) -Math.Round(d * Math.Sin(radAngle));
					// Map center-relative coordinates to window coordinates
					x += wheelBitmap.Width / 2;
					y += wheelBitmap.Width / 2;
					c = ColorMath.ToGray(ColorMath.HslToRgb(new HslColor(sHue, 255, 128))) > 128 ? Color.Black : Color.White;
					//using (Pen p = new Pen(Color.FromArgb(128, c)))
					//{
					//    pe.Graphics.DrawRectangle(p, x - 2, y - 2, 4, 4);
					//}
					using (Brush b = new SolidBrush(Color.FromArgb(128, c)))
					{
						pe.Graphics.FillRectangle(b, x - 2, y - 2, 4, 4);
					}
				}
			}

			// Draw inner color marker
			x = slBitmap.Width / 2 + saturation * (slBitmap.Width - 1) / 255;
			y = slBitmap.Width / 2 + lightness * (slBitmap.Width - 1) / 255;
			c = ColorMath.ToGray(ColorMath.HslToRgb(new HslColor(hue, saturation, lightness))) > 128 ? Color.Black : Color.White;
			using (Pen p = new Pen(c))
			{
				pe.Graphics.DrawEllipse(p, x - 3, y - 3, 6, 6);
			}
		}

		protected override void OnMouseDown(MouseEventArgs e)
		{
			if (e.Button == MouseButtons.Left)
			{
				int halfSLWidth = slBitmap.Width / 2;
				if (e.X >= halfSLWidth && e.X < halfSLWidth * 3 &&
					e.Y >= halfSLWidth && e.Y < halfSLWidth * 3)
				{
					draggingSL = true;
					OnMouseMove(e);
				}
				else
				{
					int halfWheelWidth = wheelBitmap.Width / 2;
					Point center = new Point(halfWheelWidth, halfWheelWidth);
					double dist = GetDistance(new Point(e.X, e.Y), center);
					if (dist >= halfWheelWidth * 0.78 && dist < halfWheelWidth)
					{
						draggingHue = true;
						OnMouseMove(e);
					}
				}
			}

			base.OnMouseDown(e);
		}
		protected override void OnMouseUp(MouseEventArgs e)
		{
			draggingSL = false;
			draggingHue = false;

			base.OnMouseUp(e);
		}

		protected override void OnMouseMove(MouseEventArgs e)
		{
			if (e.Button == MouseButtons.Left)
			{
				if (draggingSL)
				{
					int halfSLWidth = slBitmap.Width / 2;
					Saturation = (byte) Math.Max(0, Math.Min(255, (e.X - halfSLWidth) * 255 / slBitmap.Width));
					Lightness = (byte) Math.Max(0, Math.Min(255, (e.Y - halfSLWidth) * 255 / slBitmap.Width));
					OnSLChanged();
				}
				else if (draggingHue)
				{
					int halfWheelWidth = wheelBitmap.Width / 2;
					Point center = new Point(halfWheelWidth, halfWheelWidth);
					double radAngle = Math.Atan2(e.Y - center.Y, e.X - center.X);
					double factor = 128.0 / Math.PI;   // map -pi...pi to 0...255 => map 0...pi to 0...128
					// Calculation notes see PrepareWheelBitmap()
					Hue = (byte) Mod((int) (-factor * radAngle + 192), 256);
					OnHueChanged();
				}
			}
			
			base.OnMouseMove(e);
		}

		private void PrepareWheelBitmap()
		{
			if (wheelBitmap != null)
				wheelBitmap.Dispose();

			int width = Math.Min(ClientSize.Width, ClientSize.Height);
			Point center = new Point(width / 2, width / 2);
			if (width < 10)
			{
				wheelBitmap = null;
				return;
			}
			
			// Prepare Bitmap
			wheelBitmap = new Bitmap(width, width);

			// Fill background
			Graphics g = Graphics.FromImage(wheelBitmap);
			using (Brush b = new SolidBrush(Color.Transparent))
			{
				g.FillRectangle(b, 0, 0, width, width);
			}

			// Paint outer color wheel
			double minDist = width / 2 * 0.78;
			double maxDist = width / 2 - 1;
			double factor = 128.0 / Math.PI;   // map -pi...pi to 0...255 => map 0...pi to 0...128

			BitmapData bmData;
			byte[] bytes;
			BitmapReadBytes(wheelBitmap, out bytes, out bmData);
			for (int y = 0; y < width; y++)
			{
				for (int x = 0; x < width; x++)
				{
					double dist = GetDistance(new Point(x, y), center);
					byte alpha;
					if (dist < minDist - 0.5)
						alpha = 0;
					else if (dist < minDist + 0.5)
						alpha = (byte) ((0.5 - minDist + dist) * 255);
					else if (dist < maxDist - 0.5)
						alpha = 255;
					else if (dist < maxDist + 0.5)
						alpha = (byte) ((0.5 + maxDist - dist) * 255);
					else
						alpha = 0;

					if (alpha > 0)
					{
						double radAngle = Math.Atan2(y - center.Y, x - center.X);
						// -pi   -> -180° -> 192
						// -pi/2 ->  -90° -> 128
						// 0     ->    0° ->  64
						// pi/2  ->   90° ->   0
						// pi    ->  180° -> 192
						//
						// y = a * x + t;
						// a = -1
						// t = 192
						byte hue = (byte) Mod((int) (-factor * radAngle + 192), 256);
						Color c = Color.FromArgb(alpha, ColorMath.HslToRgb(new HslColor(hue, 255, 128)));
						BitmapSetPixel(bytes, bmData, x, y, c);
					}
				}
			}
			BitmapWriteBytes(wheelBitmap, bytes, bmData);
		}

		private void PrepareSLBitmap()
		{
			if (slBitmap != null)
				slBitmap.Dispose();

			int width = Math.Min(ClientSize.Width, ClientSize.Height) / 2;
			if (width < 10)
			{
				slBitmap = null;
				return;
			}

			// Prepare Bitmap
			slBitmap = new Bitmap(width, width);

			BitmapData bmData;
			byte[] bytes;
			BitmapReadBytes(slBitmap, out bytes, out bmData);
			for (int y = 0; y < width; y++)
			{
				for (int x = 0; x < width; x++)
				{
					Color c = ColorMath.HslToRgb(new HslColor(hue, (byte) (x * 255 / width), (byte) (y * 255 / width)));
					BitmapSetPixel(bytes, bmData, x, y, c);
				}
			}
			BitmapWriteBytes(slBitmap, bytes, bmData);
		}

		private void BitmapReadBytes(Bitmap bmp, out byte[] bytes, out BitmapData bmData)
		{
			bmData = bmp.LockBits(
				new Rectangle(0, 0, bmp.Width, bmp.Height),
				ImageLockMode.ReadWrite,
				PixelFormat.Format32bppArgb);
			const int bpp = 4;
			bytes = new byte[bmp.Width * bmp.Height * bpp];
			Marshal.Copy(bmData.Scan0, bytes, 0, bytes.Length);
		}

		private void BitmapSetPixel(byte[] bytes, BitmapData bmData, int x, int y, Color c)
		{
			int i = y * bmData.Stride + x * 4;
			bytes[i] = c.B;
			bytes[i + 1] = c.G;
			bytes[i + 2] = c.R;
			bytes[i + 3] = c.A;
		}

		private void BitmapWriteBytes(Bitmap bmp, byte[] bytes, BitmapData bmData)
		{
			Marshal.Copy(bytes, 0, bmData.Scan0, bytes.Length);
			bmp.UnlockBits(bmData);
		}

		private double GetDistance(Point a, Point b)
		{
			return Math.Sqrt((a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y));
		}

		/// <summary>
		/// Computes the real modulus value, not the division remainder.
		/// This differs from the % operator only for negative numbers.
		/// </summary>
		/// <param name="dividend">Dividend.</param>
		/// <param name="divisor">Divisor.</param>
		/// <returns></returns>
		private static int Mod(int dividend, int divisor)
		{
			if (divisor <= 0) throw new ArgumentOutOfRangeException("divisor", "The divisor cannot be zero or negative.");
			int i = dividend % divisor;
			if (i < 0) i += divisor;
			return i;
		}

		protected void OnHueChanged()
		{
			if (HueChanged != null)
				HueChanged(this, EventArgs.Empty);
		}

		protected void OnSLChanged()
		{
			if (SLChanged != null)
				SLChanged(this, EventArgs.Empty);
		}
	}
}
